package semant;

import java.util.Enumeration;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.Vector;

import util.List;

import symbol.ClassInfo;
import symbol.MethodInfo;
import symbol.Symbol;
import symbol.VarInfo;
import syntaxtree.ClassDecl;
import syntaxtree.ClassDeclExtends;
import syntaxtree.ClassDeclSimple;
import syntaxtree.Formal;
import syntaxtree.MainClass;
import syntaxtree.MethodDecl;
import syntaxtree.Program; 
import syntaxtree.Statement;
import syntaxtree.Type;
import syntaxtree.VarDecl;
import syntaxtree.VisitorAdapter;

public class VisitorRecuperaTipo extends VisitorAdapter 
{
	/* guarda as msg de erro e as classes, metodos e variaveis que forem encontradas */
	private Env _env;
	
	/* classe que estah sendo atualmente tratada */
	private ClassInfo _classeAtual;               
	
	/* variavel que estah sendo atualmente tratada */
	private VarInfo	_varAtual;
	
	/* metodo que estah sendo atualmente tratado */
	private MethodInfo _metodoAtual;

	/* lista das classes que ainda nao foi descoberto o pai */
	private LinkedList<ClassDeclExtends> _lista_clasSemPai;
	
	/*
	 * @constructor
	 * @param local_env ambiente para guardar erro
	 */
	public VisitorRecuperaTipo(Env local_env)
	{
		_env = local_env;
		_lista_clasSemPai = new LinkedList<ClassDeclExtends>();
	}
	
	/*
	 * Visita a classe Program
	 * @see syntaxtree.VisitorAdapter#visit(syntaxtree.Program)
	 */
	public void visit(Program node)
	{
		/**************************
		 *  1a acao: 
		 *  tratar MainClass
		 **************************/
		node.mainClass.accept(this);		
		
		/* incluir mainClass na lista de classes*/
		if (null != _classeAtual)
		{
			
			/* insere na lista de classes*/
			if (false == _env.classes.put(_classeAtual.name, _classeAtual))
			{
				/* classe jah inserida, enviar msg de erro */
			 	Object[] msg_erro = new Object[]{"Classe principal '" + _classeAtual.name.toString() + "' redefinida"};
			 	_env.err.Error(node, msg_erro);
			}
			/* reseta var*/
			_classeAtual = null;
		}
		
		/**********************************************
		 *  2a acao: 
		 *  tratar ClassDecl*, 
		 *  ou seja, tratar as outras classes
		 **********************************************/
		/* percorre a lista de classes existentes */
		for(List<ClassDecl> lista_classes = node.classList; 
		    null != lista_classes; 
		    lista_classes = lista_classes.tail)
		{
			/* para cada classe chama o visitor */
			lista_classes.head.accept(this);
			
			/* incluir classe "head" na lista de classes*/
			if (null != _classeAtual)
			{
				/* insere na lista de classes*/
				if (false == _env.classes.put(_classeAtual.name, _classeAtual))
				{
					/* classe jah inserida, enviar msg de erro */
					Object[] msg_erro = new Object[]{"Classe '" + _classeAtual.name.toString() + "' redefinida"};
					_env.err.Error(node, msg_erro);
				}
				/* reseta var*/
				_classeAtual = null;
			}			
		}
		
		/**********************************************
		 *  3a acao:
		 *  Busca retirar da lista classes onde o pai 
		 *  foi achado e elas estao ainda como
		 *  sem pais
		 **********************************************/
		encontraPaiDasClasses(node);
		
		/**********************************************
		 *  4a acao: 
		 *  checar classes que herdam de outra classe
		 *  onde o pai ainda nao tenha sido encontrado
		 *  Casos de erro:
		 *  * Classe nao tem pai
		 *  * A extends A
		 *  * A extends B e B extends A
		 **********************************************/
		trataClasseSemPai(node);		
	}
	
	/**
	 * checar classes que herdam de outra classe
	 * verifica se o pai jah foi encontrado
	 * se foi, retira da lista de classes sem pai
	 * @param node 
	 */
	private void encontraPaiDasClasses(Program node)
	{
		/**
		 * flag que indica que uma classe foi inserida no pool
		 * de classes corretas 
		 */
		boolean houveInsercao = true;
		
		/**
		 * Enquanto conseguir retirar elementos da lista de classes sem Pai e esta lista
		 * nao for vazia, continua tentando achar o pai das classes ainda 'orfas'
		 */
		while( (true == houveInsercao) &&
			   (_lista_clasSemPai.size() > 0))
		{
			/* reseta variavel */
			houveInsercao = false;
			
			/**
			 * para cada elemento da lista de classes sem pai
			 * verifica se seu pai estah presente
			 * se sim retira o filho da lista
			 */
			int num_elementos = _lista_clasSemPai.size();
			for(int i = 0; i < num_elementos; i++)
			{
				/* para cada elemento da lista */
				ClassDeclExtends classeSemPai = _lista_clasSemPai.removeFirst();
				
				/* para cada iteracao, nova lista de pais deve ser gerada */
				Enumeration<Symbol> lista_pais = _env.classes.keys();
				
				while(true == lista_pais.hasMoreElements())
				{
					/* recupera proximo possivel pai */
					Symbol possPai = lista_pais.nextElement();
					
					if (true == classeSemPai.superClass.toString().equals(possPai.toString()))
					{
						/**
						 * achou o pai, 
						 * chama o visitor para esta classe
						 * e insere na lista de classes
						 */						
						classeSemPai.accept(this);
						
						/* incluir classe na lista de classes*/
						if (null != _classeAtual)
						{
							/* insere na lista de classes*/
							if (true == _env.classes.put(_classeAtual.name, _classeAtual))
							{
								houveInsercao = true;
							}
							else
							{
								/* classe jah inserida, enviar msg de erro */
								Object[] msg_erro = new Object[]{"Classe '" + _classeAtual.name.toString() + "' redefinida"};
								_env.err.Error(node, msg_erro);
							}
							/* reseta var*/
							_classeAtual = null;
						} /*fim if inclui classe*/					
					}/*fim if achou pai*/
				}/*while pais a verificar*/
				
				/* classe ainda sem pai, volta para a lista */
				if (false == houveInsercao)
				{
					_lista_clasSemPai.addLast(classeSemPai);
				}
			}/*for filhos sem pai*/			
		}/*fim while tem modifs*/
	}	

	/**
	 * Classes sem Pai, encontra o motivo
	 * OBS: deve ser chamado depois de chamar
	 * trataClasseSemPai
	 * Casos de erro:
	 *  * Classe tem pai, mas pai tem erro
	 *  * Classe nao tem pai
	 *  * Classe herda dela mesma
	 *  * Heranca ciclica  
	 */
	private void trataClasseSemPai(Program node)
	{
		/**
		 * encontra o pai de todos os elementos do vetor
		 * Resultado:
		 *     -1: se nao encontrou
		 *      X: posicao do pai no vetor _lista_clasSemPai
		 */
		 Vector<Integer> vet_pai = preencheVetorPai(_lista_clasSemPai);
		
		/**
		 * Colore o grafo
		 * 1 = passou uma vez (nao faz parte de ciclo)
		 * 2 = passou duas vezes (faz parte de um ciclo)
		 */
		Vector<Integer> vet_cor = coloreVetor(vet_pai);
		
		/* para cada classe sem pai */
		for (int i = 0; i < vet_cor.size(); i++)
		{
			Object[] msg_erro;
			if ( vet_pai.get(i) == -1 )
			{
				/**
				 *  pai nao encontrado 
				 */
				msg_erro = new Object[]{ "A classe '" + _lista_clasSemPai.get(i).name.toString() + "' herda de uma classe invalida." + 
				                         " Classe pai '" + _lista_clasSemPai.get(i).superClass.toString() + "' nao existe. "};
				_env.err.Error(node, msg_erro);
			}
			else if ( vet_pai.get(i) == i)
			{
				/**
				 * Classe herda dela mesma
				 */
				msg_erro = new Object[]{ "Classe '" + _lista_clasSemPai.get(i).name.toString() + "' herda dela mesma." };
				_env.err.Error(node, msg_erro);
			}
			else if ( vet_cor.get(i) == 2)
			{
				/**
				 * pode ser um ciclo de mais de uma classe,
				 * exibe erro dizendo que as classes formam um ciclo
				 */
				String msg = "Encontrado heranca ciclica entre as classes: '";
				msg += _lista_clasSemPai.get(i).name.toString();
				int atual = vet_pai.get(i);
				int anter = i;
				/* pode ser um ciclo */
				while ((atual != i) && (atual != -1))
				{
					msg += "' => '" + _lista_clasSemPai.get(atual).name.toString();
					anter = atual;
					atual = vet_pai.get(atual);
					
					if (atual != -1)
					{
						/**
						 * jah tratou atual e 
						 * pai do atual nao eh nulo 
						 */
						vet_cor.set(anter, 0);
					}
				}
				if (atual == i)
				{
					/* se caiu neste caso era um ciclo */
					msg += "' => '" + _lista_clasSemPai.get(i).name.toString() + "'";
					msg_erro = new Object[]{ msg };
					_env.err.Error(node, msg_erro);
				}
			}
			else
			{
				/*******************
				 * classe tem pai
				 * mas pai tem 
				 * problemas
				 * nao tem 
				 * erro
				 ***/
			}
			
			/* jah imprimiu item */
			vet_cor.set(i, 0);
		}
	}

	/**
	 * encontra o pai de todos os elementos do vetor
	 * -1 se nao encontrou ou
	 * X posicao do pai no vetor _lista_clasSemPai
	 */
	private Vector<Integer> preencheVetorPai(LinkedList<ClassDeclExtends> vet_classes)
	{
		Vector<Integer> vet_pai = new Vector<Integer>();
		
		/* para cada elemento do vetor */
		int pos_elem = 0;
		for( ClassDeclExtends elemento : vet_classes)
		{
			/* seta como sem pai */
			vet_pai.add(-1);
			
			/* verifica se o elemento da lista eh seu pai */
			int pos_pai = 0;
			for (ClassDeclExtends elem_pai : vet_classes)
			{
				if (elemento.superClass.toString().equals(elem_pai.name.toString()))
				{
					/* achou o pai */
					/* fala que ele eh o pai */
					vet_pai.set(pos_elem, pos_pai);
					break;
				}
				pos_pai++;
			}		
			pos_elem++;
		}
		
		/* retorna vetor */
		return vet_pai;
	}
	
	/**
	 * Colore o grafo
	 * 1 = passou uma vez (nao faz parte de ciclo)
	 * 2 = passou duas vezes (faz parte de um ciclo)
	 */
	private Vector<Integer> coloreVetor(Vector<Integer> vet_pai)
	{
		Vector<Integer> vet_cor = new Vector<Integer>();
		
		/* zera todo o grafo */
		for (int i = 0; i < vet_pai.size(); i++)
		{
			vet_cor.add(0);
		}
		
		/* colore o grafo */
		int noh_tratado = 0;
		for (Integer pos_pai : vet_pai)
		{
			/* noh nunca foi visitado? */
			if (0 == vet_cor.get(noh_tratado))
			{
				/* visita noh */
				vet_cor.set(noh_tratado, 1);
				
				int pai_atual = pos_pai;
				while ((pai_atual != -1) &&
					   (vet_cor.get(pai_atual) != 2))
				{
					/* escurece pai */
					int cor = ( vet_cor.get(pai_atual) == 2 ? 2 : vet_cor.get(pai_atual) + 1 );
					vet_cor.set(pai_atual, cor);
					
					/* proximo vertice */
					pai_atual = vet_pai.get(pai_atual);
				}
			}
			noh_tratado++;
		}
		return vet_cor;
	}

	/**
	 * ok Visita a declaracao da mainclass
	 * Visita o mainArgs, statement e 
	 * retorna a _classeAtual preenchida
	 */
	public void visit(MainClass node) 
	{
		_classeAtual = null;
		
		/**
		 * visita params
		 */
		node.mainArgName.accept(this);
		
		/**
		 * Tratar Statement
		 */
		node.s.accept(this);
		
		/**
		 * preenche a _classeAtual
		 */
		Symbol simb = Symbol.symbol(node.className.toString());
		_classeAtual = new ClassInfo(simb);
	}
	
	/**
	 * ok Visita uma definicao de classe que nao herda de ninguem
	 * verifica o tipo, guarda as variaveis e os metodos
	 */
	public void visit(ClassDeclSimple node) 
	{
		_classeAtual = null;
		
		/**********************
		 * Cria a nova variavel
		 * da classe
		 **********************/
		Symbol simb = Symbol.symbol(node.name.toString());
		_classeAtual = new ClassInfo(simb);
		
		/**********************
		 * guarda os atributos
		 * locais da classe
		 **********************/
		for (List<VarDecl> lista_atrib = node.varList;
			 null != lista_atrib;
			 lista_atrib = lista_atrib.tail)
		{
			/* para cada atributo chama o visitor */
			lista_atrib.head.accept(this);
			
			/* incluir atrib "head" na lista de atrib*/
			if (null != _varAtual)
			{
				/* insere na lista de params*/
				if (false == _classeAtual.addAttribute(_varAtual))
				{
					/* objeto jah existe nesta lista*/
					Object[] msg_erro = new Object[]{ "Atributo redefinido " + _varAtual.name.toString() };						
					_env.err.Error(node, msg_erro);					
				}
				/* reseta var*/
				_varAtual = null;
			}			
		}
		
		/**********************
		 * guarda os metodos
		 * da classe
		 **********************/
		for (List<MethodDecl> lista_met = node.methodList;
			 null != lista_met;
			 lista_met = lista_met.tail)
		{
			/* para cada metodo chama o visitor */
			lista_met.head.accept(this);
			
			/* incluir metodo "head" na lista de metodos*/
			if (null != _metodoAtual)
			{
				/* insere na lista de metodos*/
				if (false == _classeAtual.addMethod(_metodoAtual))
				{
					/* objeto jah existe nesta lista*/
					Object[] msg_erro = new Object[]{ "Metodo redefinido " + _metodoAtual.name.toString() };						
					_env.err.Error(node, msg_erro);					
				}
				/* reseta var*/
				_metodoAtual = null;
			}			
		}
		
	}
	
	/**
	 * ok Visita a declaracao de uma classe que herda de outra
	 * Verifica se o pai jah foi encontrado, se sim, classe estah 
	 * pronta para ser inserida segue o mesmo fluxo que para uma 
	 * classe simples: verifica o tipo, guarda as variaveis e os metodos
	 * se nao foi, tem de aguardar encontrar o pai
	 */
	public void visit(ClassDeclExtends node) 
	{
		_classeAtual = null;
		
		/**
		 * verifica se o pai jah foi inserido
		 */
		ClassInfo classePai = _env.classes.get(Symbol.symbol(node.superClass.toString()));
		if (null != classePai)
		{
			/* pai nao eh nulo */
			/* cria classe     */
			Symbol simb = Symbol.symbol(node.name.toString());
			_classeAtual = new ClassInfo(simb);
		
			/**********************************************************
			 * guarda as variaveis da classe
			 **********************************************************/
			for(List<VarDecl> lista_atrib = node.varList;
			    lista_atrib != null;
			    lista_atrib = lista_atrib.tail)
			{
				/* para cada atributo chama o visitor */
				lista_atrib.head.accept(this);
				
				/* incluir atrib "head" na lista de atrib*/
				if (null != _varAtual)
				{
					/* insere na lista de params*/
					if (false == _classeAtual.addAttribute(_varAtual))
					{
						/* objeto jah existe nesta lista*/
						Object[] msg_erro = new Object[]{ "Atributo redefinido " + _varAtual.name.toString() };						
						_env.err.Error(node, msg_erro);					
					}
					/* reseta var*/
					_varAtual = null;
				}
			}
						
			/**********************************************************
			 * guarda os metodos da classe
			 * 1 - metodo herdado? assinatura tem de ser igual
			 * 2 - metodo proprio? tratamento como na classe simples 
			 **********************************************************/
			Hashtable<Symbol,MethodInfo> lista_metPai = classePai.methods;
			for (List<MethodDecl> lista_met = node.methodList;
			     null != lista_met;
			     lista_met = lista_met.tail)
			{
				/* pai tem metodo com mesmo "nome"? */
				MethodInfo metodoPai = lista_metPai.get(Symbol.symbol(lista_met.head.name.toString()));
				
				if (null != metodoPai)
				{
					/* classe filha sobreescreve o metodo */
					
					/**************************************
					 * verifica se assinatura eh igual 
					 * - tipo dos argumentos iguais
					 * - numero de parametros iguais
					 * - retorno igual
					 **************************************/ 
					List<VarInfo> lista_paramPai   = metodoPai.formals;
					List<Formal>  lista_paramFilho = lista_met.head.formals;
					
					/* testa os tipos dos parametros */
					Integer pos = 1;
					/* verifica se as listas sao iguais */
					while ((null != lista_paramFilho) &&
						   (null != lista_paramPai))
					{	
						/* verifica se os tipos sao iguais */
						if(false == lista_paramFilho.head.type.toString().equals((lista_paramPai.head.type.toString())))
						{
							/* tipos diferentes, adiciona erro */
							Object[] msg_erro = new Object[]{ "O tipo encontrado '" + lista_paramFilho.head.type.toString() + "'" + " no " + pos.toString() + 
									                          "o parametro do metodo reimplementado '" + metodoPai.name.toString() + 
								                              "' nao corresponde ao tipo '" + lista_paramPai.head.type.toString() + "' encontrado na classe pai"};
							_env.err.Error(node, msg_erro);
						}	
						/*incrementa*/
						lista_paramFilho = lista_paramFilho.tail;
						lista_paramPai   = lista_paramPai.tail;
						pos++;
					}
					
					/* testa se o numero de parametros eh igual */
					if ((null != lista_paramFilho) && ( null == lista_paramPai)) 
					{
						/* numero de argumentos no filho > que no pai */
						Object[] msg_erro = new Object[]{ "O metodo reimplementado '" + metodoPai.name.toString() + 
							                              "' possui mais parametros na classe filha que na classe pai"};
						_env.err.Error(node, msg_erro);
					}
					else if ((null == lista_paramFilho) && ( null != lista_paramPai)) 
					{
						/* numero de argumentos no filho < que no pai */
						Object[] msg_erro = new Object[]{ "O metodo reimplementado '" + metodoPai.name.toString() + 
							                              "' possui mais parametros na classe pai que na classe filha"};
						_env.err.Error(node, msg_erro);
					
					}

					/* testa se o retorno eh igual   */
					if(false == metodoPai.type.toString().equals(lista_met.head.returnType.toString()))
					{
						Object[] msg_erro = new Object[]{"O tipo de retorno encontrado '" + lista_met.head.returnType.toString() + 
								                         "' no metodo reimplementado '" + metodoPai.name.toString() + 
								                         "' nao corresponde ao tipo '" + metodoPai.type.toString() + "' encontrado na classe pai"};
						_env.err.Error(node, msg_erro);
					}

				} /*fim if metodo derivado */
				else
				{
					/* metodo proprio da classe filha */
					
					/* chama o visitor */
					lista_met.head.accept(this);
					
					/* incluir metodo "head" na lista de metodos*/
					if (null != _metodoAtual)
					{
						/* insere na lista de metodos*/
						if (false == _classeAtual.addMethod(_metodoAtual))
						{
							/* objeto jah existe nesta lista*/
							Object[] msg_erro = new Object[]{ "Metodo '" + _metodoAtual.name.toString() + "' redefinido" };						
							_env.err.Error(node, msg_erro);					
						}
						/* reseta var*/
						_metodoAtual = null;
					}
				}/*fim else metodo derivado*/
			}/*fim for tem metodos*/	

			/** 
			 * 'Copia' os atributos da classe base
			 */
			_classeAtual.setBase(classePai);

		}/*fim if pai jah inserido*/
		else 
		{
			/* nao sabe quem eh o pai */
			_lista_clasSemPai.add(node);
		}
	}

	/**
	 * ok Visita a declaracao de variavel
	 * Popula a variavel temp que serah usada por outro metodo para verificar
	 * se deve inserir a variavel na lista e nao existe erro parametragem
	 */
	public void visit(VarDecl node) 
	{
		Symbol simb = Symbol.symbol(node.name.toString());
		Type   tipo = node.type;
		_varAtual   = new VarInfo(tipo,simb);
	}

	/**
	 * ok Visita a declaracao de metodo
	 * recupera tipo do metodo, dos parametros e das variaveis locais
	 */
	public void visit(MethodDecl node)
	{
		_metodoAtual = null;
		
		/**************************
		 * Cria _metodoAtual
		 **************************/
		Type   tipo = node.returnType;
		Symbol simb = Symbol.symbol(node.name.toString());		
		_metodoAtual = new MethodInfo(tipo, simb, _classeAtual.name); 
		
		/**************************
		 * trata os parametros do 
		 * metodo
		 **************************/
		for(List<Formal> lista_params = node.formals;
		    null != lista_params;
		    lista_params = lista_params.tail)
		{
			/* para cada parametro chama o visitor */
			lista_params.head.accept(this);
			
			/* incluir param "head" na lista de vars*/
			if (null != _varAtual)
			{
				/* insere na lista de params*/
				if (false == _metodoAtual.addFormal(_varAtual))
				{
					Object[] msg_erro;
					/* dois erros possiveis :*/
					
					/* jah contem parametro */
					if(true == _metodoAtual.formalsTable.containsKey(_varAtual.name))
					{
						msg_erro = new Object[]{ "Parametro redefinido " + _varAtual.name.toString() };
					}
					/* contem variavel local com mesmo nome */
					else if (true == _metodoAtual.localsTable.containsKey(_varAtual.name))
					{
						msg_erro = new Object[]{ "Parametro possui mesmo nome da variavel local " + _varAtual.name.toString() };
					}
					else
					{
						/* NAO DEVE ENTRAR AQUI NUNCA a menos que seja modificado o 
						 * _metodoAtual.formalsTable.containsKey */
						msg_erro = new Object[]{ "Erro desconhecido com a variavel!"};
					}						
					_env.err.Error(node, msg_erro);					
				}				
				/* reseta var*/
				_varAtual = null;
			}			
		}
		
		/**************************
		 * trata as variaveis 
		 * locais do metodo
		 **************************/
		for(List<VarDecl> lista_vars = node.locals;
		    null != lista_vars;
		    lista_vars = lista_vars.tail)
		{
			/* para cada variavel chama o visitor */
			lista_vars.head.accept(this);
			
			/* incluir var "head" na lista de vars*/
			if (null != _varAtual)
			{
				/* insere na lista de vars*/
				if (false == _metodoAtual.addLocal(_varAtual))
				{
					Object[] msg_erro;
					/* dois erros possiveis :*/
					
					/* jah contem variavel */
					if(true == _metodoAtual.localsTable.containsKey(_varAtual.name))
					{
						msg_erro = new Object[]{ "Variavel '" + _varAtual.name.toString() + "' redefinida" };
					}
					/* contem parametro com mesmo nome */
					else if (true == _metodoAtual.formalsTable.containsKey(_varAtual.name))
					{
						msg_erro = new Object[]{ "Variavel local possui mesmo nome de parametro '" + _varAtual.name.toString() + "'" };
					}
					else
					{
						/* NAO DEVE ENTRAR AQUI NUNCA A MENOS QUE SEJA MODIFICADO AS CONDICOES DE 
						 * _metodoAtual.localsTable.containsKey  */
						msg_erro = new Object[]{ "Erro desconhecido com a variavel!"};
					}						
					_env.err.Error(node, msg_erro);					
				}				
				/* reseta var*/
				_varAtual = null;
			}			
		}
		
		/**************************
		 * trata Statments do metodo
		 **************************/
		for(List<Statement> lista_stat = node.body;
		    null != lista_stat; 
		    lista_stat = lista_stat.tail)
		{
			lista_stat.head.accept(this);
		}
		
		/**************************
		 * trata return do metodo
		 **************************/
		node.returnExp.accept(this);		
	}

	/**
	 * ok Visita a declaracao de parametro
	 * Popula a variavel temp que serah usada por outro metodo para verificar
	 * se deve inserir a variavel na lista e nao existe erro parametragem
	 */
	public void visit(Formal node) 
	{
		Symbol simb = Symbol.symbol(node.name.toString());
		Type   tipo = node.type;
		_varAtual   = new VarInfo(tipo,simb);
	}
		
}
